/*
 * Copyright 2011 James Moger.
 * Copyright 2012 Frédéric Gaillard
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.iciql;

import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.regex.Pattern;

/**
 * Iciql wraps all exceptions with this class.
 */
public class IciqlException extends RuntimeException {

    public static final int CODE_UNMAPPED_FIELD = 1;
    public static final int CODE_DUPLICATE_KEY = 2;
    public static final int CODE_OBJECT_NOT_FOUND = 3;
    public static final int CODE_OBJECT_ALREADY_EXISTS = 4;
    public static final int CODE_CONSTRAINT_VIOLATION = 5;
    public static final int CODE_UNCHARACTERIZED = 6;

    private static final String TOKEN_UNMAPPED_FIELD = "\\? (=|\\>|\\<|\\<\\>|!=|\\>=|\\<=|LIKE|BETWEEN) \\?";

    private static final long serialVersionUID = 1L;

    private String sql;

    private int iciqlCode;

    public IciqlException(Throwable t) {
        super(t.getMessage(), t);
        configureCode(t);
    }

    public IciqlException(String message, Object... parameters) {
        super(parameters.length > 0 ? MessageFormat.format(message, parameters) : message);
    }

    public IciqlException(Throwable t, String message, Object... parameters) {
        super(parameters.length > 0 ? MessageFormat.format(message, parameters) : message, t);
        configureCode(t);
    }

    public static void checkUnmappedField(String sql) {
        if (Pattern.compile(IciqlException.TOKEN_UNMAPPED_FIELD).matcher(sql).find()) {
            IciqlException e = new IciqlException("unmapped field in statement!");
            e.sql = sql;
            e.iciqlCode = CODE_UNMAPPED_FIELD;
            throw e;
        }
    }

    public static IciqlException fromSQL(String sql, Throwable t) {
        if (Pattern.compile(TOKEN_UNMAPPED_FIELD).matcher(sql).find()) {
            IciqlException e = new IciqlException(t, "unmapped field in statement!");
            e.sql = sql;
            e.iciqlCode = CODE_UNMAPPED_FIELD;
            return e;
        } else {
            IciqlException e = new IciqlException(t, t.getMessage());
            e.sql = sql;
            return e;
        }
    }

    public void setSQL(String sql) {
        this.sql = sql;
    }

    public String getSQL() {
        return sql;
    }

    public int getIciqlCode() {
        return iciqlCode;
    }

    private void configureCode(Throwable t) {
        if (t == null) {
            return;
        }
        if (t instanceof SQLException) {
            // http://developer.mimer.com/documentation/html_92/Mimer_SQL_Mobile_DocSet/App_Return_Codes2.html
            SQLException s = (SQLException) t;
            String state = s.getSQLState();
            if ("23000".equals(state)) {
                // MySQL duplicate primary key on insert
                iciqlCode = CODE_DUPLICATE_KEY;
                if (s.getErrorCode() == 1217) {
                    iciqlCode = CODE_CONSTRAINT_VIOLATION;
                }
            } else if ("23505".equals(state)) {
                // Derby duplicate primary key on insert
                iciqlCode = CODE_DUPLICATE_KEY;
            } else if ("42000".equals(state)) {
                // MySQL duplicate unique index value on insert
                iciqlCode = CODE_DUPLICATE_KEY;
            } else if ("42Y07".equals(state)) {
                // Derby schema not found
                iciqlCode = CODE_OBJECT_NOT_FOUND;
            } else if ("42X05".equals(state)) {
                // Derby table not found
                iciqlCode = CODE_OBJECT_NOT_FOUND;
            } else if ("42Y55".equals(state)) {
                // Derby table not found
                iciqlCode = CODE_OBJECT_NOT_FOUND;
            } else if ("42S02".equals(state)) {
                // H2 table not found
                iciqlCode = CODE_OBJECT_NOT_FOUND;
            } else if ("42501".equals(state)) {
                // HSQL table not found
                iciqlCode = CODE_OBJECT_NOT_FOUND;
            } else if ("42P01".equals(state)) {
                // PostgreSQL table not found
                iciqlCode = CODE_OBJECT_NOT_FOUND;
            } else if ("X0X05".equals(state)) {
                // Derby view/table not found exists
                iciqlCode = CODE_OBJECT_NOT_FOUND;
            } else if ("X0Y32".equals(state)) {
                // Derby table already exists
                iciqlCode = CODE_OBJECT_ALREADY_EXISTS;
            } else if ("42P07".equals(state)) {
                // PostgreSQL table or index already exists
                iciqlCode = CODE_OBJECT_ALREADY_EXISTS;
            } else if ("42S01".equals(state)) {
                // MySQL view already exists
                iciqlCode = CODE_OBJECT_ALREADY_EXISTS;
            } else if ("42S11".equals(state)) {
                // H2 index already exists
                iciqlCode = CODE_OBJECT_ALREADY_EXISTS;
            } else if ("42504".equals(state)) {
                // HSQL index already exists
                iciqlCode = CODE_OBJECT_ALREADY_EXISTS;
            } else if ("2BP01".equals(state)) {
                // PostgreSQL constraint violation
                iciqlCode = CODE_CONSTRAINT_VIOLATION;
            } else if ("42533".equals(state)) {
                // HSQL constraint violation
                iciqlCode = CODE_CONSTRAINT_VIOLATION;
            } else if ("X0Y25".equals(state)) {
                // Derby constraint violation
                iciqlCode = CODE_CONSTRAINT_VIOLATION;
            } else if (s.getMessage().startsWith("[SQLITE")) {
                // SQLite error codes
                final String msg = s.getMessage();
                switch (s.getErrorCode()) {
                    case 1:
                        iciqlCode = CODE_OBJECT_NOT_FOUND;
                        break;
                    case 19:
                        if (msg.contains("UNIQUE")) {
                            iciqlCode = CODE_DUPLICATE_KEY;
                        } else {
                            iciqlCode = CODE_CONSTRAINT_VIOLATION;
                        }
                        break;
                    default:
                        iciqlCode = s.getErrorCode();
                        break;
                }
            } else {
                // uncharacterized SQL code, we can always rely on iciqlCode != 0 in IciqlException
                iciqlCode = s.getErrorCode() == 0 ? CODE_UNCHARACTERIZED : s.getErrorCode();
            }
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getName());
        String message = getLocalizedMessage();
        if (message != null) {
            sb.append(": ").append(message);
        }
        if (sql != null) {
            sb.append('\n').append(sql);
        }
        return sb.toString();
    }
}
